package cn.bugstack.mybatis.datasource.pooled;

import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.logging.Logger;

import javax.sql.DataSource;

import cn.bugstack.mybatis.datasource.unpooled.UnpooledDataSource;
import org.slf4j.LoggerFactory;

/**
 * description: 有池化数据源
 * taskId：
 * <pre>
 * change history:
 * date             defect#             person                comments
 * ---------------------------------------------------------------------
 * 2024/11/29       ********         zhaochaoyue        create file
 * </pre>
 *
 * @author: zhaochaoyue
 * @date: 2024/11/29 10:18
 * @copyright: 2024
 */
public class PooledDataSource implements DataSource {
    
    private org.slf4j.Logger logger = LoggerFactory.getLogger(PooledDataSource.class);
    
    //池状态
    private final PoolState state = new PoolState(this); 
    
    private final UnpooledDataSource dataSource;
    
    //活跃连接数
    protected int poolMaximumActiveConnections =10;
    //空闲连接数
    protected int poolMaximumIdleConnections = 5;
    //在被强制返回之前 池中连接的检查时间
    protected int poolMaximumCheckoutTime =20000;
    //这是给连接池一个打印日志状态机会的低层次设置 使用重新尝试获取连接 为了避免连接池没有配置时静默失败
    protected int poolTimeToWait =20000;
    //发送数据到侦查查询用来检查连接是否正常工作 并且准备接受请求默认为 “NO PING QUERY SET ”
    protected String poolPingQuery ="NO PING QUERY SET";
    //开启禁用侦查查询
    protected boolean poolPingEnabled = false;
    //用来配置poolPingQuery 多次时间被用一次
    protected int poolPingConnectionsNotUseFor = 0;
    
    private int expectedConnectionTypeCode;
    
    public PooledDataSource() {
        this.dataSource = new UnpooledDataSource();
    }
    
    protected void pushConnection(PooledConnection connection) throws SQLException {
        synchronized (state){
            //判断连接是否有效
            if(connection.isValid()){
                //如果空闲链接小于设定数量 也就是太少时
                if(state.idelConnections.size() <poolMaximumActiveConnections 
                    && connection.getConnectionTypeCode()== expectedConnectionTypeCode){
                    state.accumulatedCheckoutTime +=connection.getCheckoutTimestamp();
                    //检查数据库连接是否是自动提交模式 如果不是 则调用回滚操作
                    //MyBatis 中 如果没有开启自动提交模式 则需要手动提交或者回滚 因此这段代码可能是在完成操作后
                    //没有开启自动提交模式 则执行回滚操作
                    //总体来说 这段代码是用于保证数据库的一致性
                    if(!connection.getRealConnection().getAutoCommit()){
                        connection.getRealConnection().rollback();
                    }
                    //实例化一个新的DB连接 加入到idle 列表
                    PooledConnection newConnection = new PooledConnection(connection.getRealConnection(),this);
                    state.idelConnections.add(newConnection);
                    newConnection.setCreatedTimestamp(connection.getCreatedTimestamp());
                    newConnection.setLastUsedTimestamp(connection.getLastUsedTimestamp());
                    connection.invalidate();

                    //通知其他现场可以来抢DB链接了
                    state.notify();
                }
                //否则 空闲连接比较充足
                else {
                    state.accumulatedCheckoutTime+=connection.getCheckoutTimestamp();
                    if(!connection.getRealConnection().getAutoCommit()){
                        connection.getRealConnection().rollback();
                    }
                    //将connection 关闭
                    connection.getRealConnection().close();
                    logger.info("Closed connection "+connection.getRealHashCode()+".");
                    connection.invalidate();
                }
            }else {
                logger.info("A bad connection（"+connection.getRealHashCode()+") attempted to return to the pool,discarding connection.");
                state.badConnectionCount++;
            }
            
        }
    }
    
    private PooledConnection popConnection(String username,String password) throws SQLException {
        boolean countedWait = false;
        PooledConnection coon =null;
        long t = System.currentTimeMillis();
        int localBadConnectionCount = 0;
        while (coon ==null){
            synchronized (state){
                //如果有空闲连接 返回第一个
                if(!state.idelConnections.isEmpty()){
                    coon = state.idelConnections.remove(0);
                    logger.info("Check out connection "+coon.getRealHashCode()+ "from pool.");
                }
                //如果无空闲连接 创建新的连接
                else {
                    //活跃连接数不足
                    if(state.activeConnections.size() <poolMaximumActiveConnections){
                        coon = new PooledConnection(dataSource.getConnection(), this);
                        logger.info("Created connection "+coon.getRealHashCode()+".");
                    }
                    //活跃连接数满 
                    else {
                        //取得活跃连接第一个也就是最老的一个连接
                        PooledConnection oldestActiveConnection = state.activeConnections.get(0);
                        long longestCheckoutTime = oldestActiveConnection.getCheckoutTimestamp();
                        //如果checkout 时间过长  这个连接标记为过期
                        if(longestCheckoutTime>poolMaximumCheckoutTime){
                            state.claimedOverdueConnectionCount++;
                            state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                            state.accumulatedCheckoutTime+= longestCheckoutTime;
                            state.activeConnections.remove(oldestActiveConnection);
                            if(!oldestActiveConnection.getRealConnection().getAutoCommit()){
                                oldestActiveConnection.getRealConnection().rollback();
                            }
                            //删除最老的连接 然后重新实例化一个新的连接
                            coon = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                            oldestActiveConnection.invalidate();
                            logger.info("Claimed overdue connection"+coon.getRealHashCode()+".");
                        }
                        //如果checkout 超时时间不够长 则等待
                        else {
                            try {
                                if(!countedWait){
                                    state.hadToWaitCount++;
                                    countedWait =true;
                                }
                                logger.info("Waiting as long as "+poolTimeToWait+"milliseconds for connection.");
                                long wt = System.currentTimeMillis();
                                state.wait(poolTimeToWait); 
                                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
                            }catch (InterruptedException e){
                                break;
                            }
                            
                        }
                    }
                }
                //获取新连接
                if(coon != null){
                    if(coon.isValid()){
                        if(!coon.getRealConnection().getAutoCommit()){
                            coon.getRealConnection().rollback();
                        }
                        coon.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username,password));
                        //记录checkout 时间
                        coon.setCheckoutTimestamp(System.currentTimeMillis());
                        coon.setLastUsedTimestamp(System.currentTimeMillis());
                        state.activeConnections.add(coon);
                        state.requestCount++;
                        state.accumulatedRequestTime += System.currentTimeMillis() -t;
                    }else {
                        logger.info("A bad connection ("+coon.getRealHashCode()+") was returned from the pool, getting another connection.");
                        //如果没拿到 统计信息失败连接+i
                        state.badConnectionCount++;
                        localBadConnectionCount++;
                        coon = null;
                        //失败次数较多 抛出异常
                        if(localBadConnectionCount>(poolMaximumIdleConnections+3)){
                            logger.debug("PooledDataSource: Could not get a good connection to the database.");
                            throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
                        }
                    }
                }
            }
        }
        if(coon ==null){
            logger.debug("PooledDataSource:Unkown server error condition.The connection pool returned a null connection.");
            throw new SQLException("PooledDataSource:Unkown server error condition.The connection pool returned a null connection.");
        }
        return coon;
    }
    
    public void forceCloseAll(){
        synchronized (state){
            expectedConnectionTypeCode = assembleConnectionTypeCode(
                dataSource.getUrl(),dataSource.getUsername(),dataSource.getPassword()
            );
            //关闭活跃连接
            for (int i = state.activeConnections.size(); i >0 ; i--) {
                try {
                    PooledConnection connection = state.activeConnections.remove(i - 1);
                    connection.invalidate();
                    Connection realConn = connection.getRealConnection();
                    if(!realConn.getAutoCommit()){
                        realConn.rollback();
                    }
                    realConn.close();
                } catch (Exception ignor){
                    
                }
            }
            //关闭空闲连接
            for (int i = state.idelConnections.size(); i >0 ; i--) {
                try {
                    PooledConnection conn = state.idelConnections.remove(i - 1);
                    conn.invalidate();
                    Connection realConnection = conn.getRealConnection();
                    if(!realConnection.getAutoCommit()){
                        realConnection.rollback();
                    }
                } catch (Exception ignore){
                    
                }
               
            }
            logger.info("PooledDataSource forcefully closed/removed all connections.");
        }
    }

    

    protected boolean pingConnection(PooledConnection conn){
        boolean result = true;
        try {
            result = !conn.getRealConnection().isClosed();
        } catch (SQLException e) {
            logger.info("Connection "+conn.getRealHashCode()+"is BAD:"+e.getMessage());
            result =false;
        }
        if(result){
           if(poolPingEnabled){
               if(poolPingConnectionsNotUseFor >=0 && conn.getTimeElapsedSinceLastUse() >poolPingConnectionsNotUseFor){
                   try {
                       logger.info("Testing connection "+conn.getRealHashCode()+" ...");
                       Connection realCoon = conn.getRealConnection();
                       Statement statement = realCoon.createStatement();
                       ResultSet resultSet = statement.executeQuery(poolPingQuery);
                       resultSet.close();
                       if(!realCoon.getAutoCommit()){
                           realCoon.rollback();
                       }
                       logger.info("Connection " +conn.getRealHashCode()+" is GOOD!");

                   }catch (Exception e){
                       logger.info("Execution of ping query ‘"+poolPingQuery+"' failed:"+ e.getMessage());
                       try {
                           conn.getRealConnection().close();
                       } catch (SQLException ignore) {
                       }
                       result =false;
                       logger.info("Connection "+conn.getRealHashCode() +" is BAD:"+e.getMessage());
                   }
                   

               }
           }
        }
        return result;
    }

    public static Connection unwrapConnection(Connection conn){
        if(Proxy.isProxyClass(conn.getClass())){
            InvocationHandler handler = Proxy.getInvocationHandler(conn);
            if(handler instanceof PooledConnection){
                return ((PooledConnection)handler).getRealConnection();
            }
        }
        return conn;
    }
    
    private int assembleConnectionTypeCode(String url, String username, String password) {
        return (""+url+username+password).hashCode();
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        return popConnection(dataSource.getUsername(), dataSource.getPassword()).getRealConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return popConnection(username,password).getRealConnection();
    }

    
    protected  void  finalize() throws Throwable {
        forceCloseAll();
        super.finalize();
    }
    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        throw  new SQLException(getClass().getName()+"is not a wrapper.");
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return DriverManager.getLogWriter();
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
        DriverManager.setLogWriter(out);
    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        DriverManager.setLoginTimeout(seconds);
    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return DriverManager.getLoginTimeout();
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
    }
    
    
    public void setDriver(String driver){
        dataSource.setDriver(driver);
        forceCloseAll();
    }
    
    public void setUrl(String url){
       dataSource.setUrl(url); 
       forceCloseAll();
    }
    
    public void setUsername(String username){
        dataSource.setUsername(username);
        forceCloseAll();
    }
    
    public void setPassword(String password){
        dataSource.setPassword(password);
        forceCloseAll();
    }
    
    public void setDefaultAutoCommit(boolean defaultAutoCommit){
        dataSource.setAutoCommit(defaultAutoCommit);
        forceCloseAll();
    }
    

    public PoolState getState() {
        return state;
    }

    public UnpooledDataSource getDataSource() {
        return dataSource;
    }

    public int getPoolMaximumActiveConnections() {
        return poolMaximumActiveConnections;
    }

    public void setPoolMaximumActiveConnections(int poolMaximumActiveConnections) {
        this.poolMaximumActiveConnections = poolMaximumActiveConnections;
    }

    public int getPoolMaximumIdleConnections() {
        return poolMaximumIdleConnections;
    }

    public void setPoolMaximumIdleConnections(int poolMaximumIdleConnections) {
        this.poolMaximumIdleConnections = poolMaximumIdleConnections;
    }

    public int getPoolMaximumCheckoutTime() {
        return poolMaximumCheckoutTime;
    }

    public void setPoolMaximumCheckoutTime(int poolMaximumCheckoutTime) {
        this.poolMaximumCheckoutTime = poolMaximumCheckoutTime;
    }

    public int getPoolTimeToWait() {
        return poolTimeToWait;
    }

    public void setPoolTimeToWait(int poolTimeToWait) {
        this.poolTimeToWait = poolTimeToWait;
    }

    public String getPoolPingQuery() {
        return poolPingQuery;
    }

    public void setPoolPingQuery(String poolPingQuery) {
        this.poolPingQuery = poolPingQuery;
    }

    public boolean isPoolPingEnabled() {
        return poolPingEnabled;
    }

    public void setPoolPingEnabled(boolean poolPingEnabled) {
        this.poolPingEnabled = poolPingEnabled;
    }

    public int getPoolPingConnectionsNotUseFor() {
        return poolPingConnectionsNotUseFor;
    }

    public void setPoolPingConnectionsNotUseFor(int poolPingConnectionsNotUseFor) {
        this.poolPingConnectionsNotUseFor = poolPingConnectionsNotUseFor;
    }

    public int getExpectedConnectionTypeCode() {
        return expectedConnectionTypeCode;
    }

    public void setExpectedConnectionTypeCode(int expectedConnectionTypeCode) {
        this.expectedConnectionTypeCode = expectedConnectionTypeCode;
    }
    
}
